import { Vector3, Vector4 } from '@utils/vector';
import { _range } from '@utils/functions';

export default class Matrix4 {
  public elements: Float32Array;

  constructor(opt?: { elements: Float32Array }) {
    if (opt) {
      let { elements: source }: { elements: Float32Array } = opt;
      this.elements = source.map(item => item);
    } else {
      this.elements = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
    }
  }

  public identity(): Matrix4 {
    const { elements } = this;
    [
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    ].forEach((v, i) => elements[i] = v);
    return this;
  }

  public set(src: Matrix4): Matrix4 {
    const { elements: another } = src;
    const { elements } = this;
    if (elements === another) {
      return;
    }
    this.elements = another.map(v => v);
    return this;
  }

  public transpose(): Matrix4 {
    const { elements } = this;
    [elements[4], elements[1]] = [elements[1], elements[4]];
    [elements[8], elements[2]] = [elements[2], elements[8]];
    [elements[12], elements[3]] = [elements[3], elements[12]];
    [elements[1], elements[4]] = [elements[6], elements[9]];
    [elements[1], elements[4]] = [elements[7], elements[13]];
    [elements[1], elements[4]] = [elements[11], elements[14]];

    return this;
  }

  public setInverseOf({ elements: s }) {
    const { elements } = this;
    const inv = new Float32Array(16);

    inv[0] = s[5] * s[10] * s[15] - s[5] * s[11] * s[14] - s[9] * s[6] * s[15]
      + s[9] * s[7] * s[14] + s[13] * s[6] * s[11] - s[13] * s[7] * s[10];

    inv[4] = -s[4] * s[10] * s[15] + s[4] * s[11] * s[14] + s[8] * s[6] * s[15]
      - s[8] * s[7] * s[14] - s[12] * s[6] * s[11] + s[12] * s[7] * s[10];

    inv[8] = s[4] * s[9] * s[15] - s[4] * s[11] * s[13] - s[8] * s[5] * s[15]
      + s[8] * s[7] * s[13] + s[12] * s[5] * s[11] - s[12] * s[7] * s[9];

    inv[12] = -s[4] * s[9] * s[14] + s[4] * s[10] * s[13] + s[8] * s[5] * s[14]
      - s[8] * s[6] * s[13] - s[12] * s[5] * s[10] + s[12] * s[6] * s[9];

    inv[1] = -s[1] * s[10] * s[15] + s[1] * s[11] * s[14] + s[9] * s[2] * s[15]
      - s[9] * s[3] * s[14] - s[13] * s[2] * s[11] + s[13] * s[3] * s[10];

    inv[5] = s[0] * s[10] * s[15] - s[0] * s[11] * s[14] - s[8] * s[2] * s[15]
      + s[8] * s[3] * s[14] + s[12] * s[2] * s[11] - s[12] * s[3] * s[10];

    inv[9] = -s[0] * s[9] * s[15] + s[0] * s[11] * s[13] + s[8] * s[1] * s[15]
      - s[8] * s[3] * s[13] - s[12] * s[1] * s[11] + s[12] * s[3] * s[9];

    inv[13] = s[0] * s[9] * s[14] - s[0] * s[10] * s[13] - s[8] * s[1] * s[14]
      + s[8] * s[2] * s[13] + s[12] * s[1] * s[10] - s[12] * s[2] * s[9];

    inv[2] = s[1] * s[6] * s[15] - s[1] * s[7] * s[14] - s[5] * s[2] * s[15]
      + s[5] * s[3] * s[14] + s[13] * s[2] * s[7] - s[13] * s[3] * s[6];

    inv[6] = -s[0] * s[6] * s[15] + s[0] * s[7] * s[14] + s[4] * s[2] * s[15]
      - s[4] * s[3] * s[14] - s[12] * s[2] * s[7] + s[12] * s[3] * s[6];

    inv[10] = s[0] * s[5] * s[15] - s[0] * s[7] * s[13] - s[4] * s[1] * s[15]
      + s[4] * s[3] * s[13] + s[12] * s[1] * s[7] - s[12] * s[3] * s[5];

    inv[14] = -s[0] * s[5] * s[14] + s[0] * s[6] * s[13] + s[4] * s[1] * s[14]
      - s[4] * s[2] * s[13] - s[12] * s[1] * s[6] + s[12] * s[2] * s[5];

    inv[3] = -s[1] * s[6] * s[11] + s[1] * s[7] * s[10] + s[5] * s[2] * s[11]
      - s[5] * s[3] * s[10] - s[9] * s[2] * s[7] + s[9] * s[3] * s[6];

    inv[7] = s[0] * s[6] * s[11] - s[0] * s[7] * s[10] - s[4] * s[2] * s[11]
      + s[4] * s[3] * s[10] + s[8] * s[2] * s[7] - s[8] * s[3] * s[6];

    inv[11] = -s[0] * s[5] * s[11] + s[0] * s[7] * s[9] + s[4] * s[1] * s[11]
      - s[4] * s[3] * s[9] - s[8] * s[1] * s[7] + s[8] * s[3] * s[5];

    inv[15] = s[0] * s[5] * s[10] - s[0] * s[6] * s[9] - s[4] * s[1] * s[10]
      + s[4] * s[2] * s[9] + s[8] * s[1] * s[6] - s[8] * s[2] * s[5];

    let det = s[0] * inv[0] + s[1] * inv[4] + s[2] * inv[8] + s[3] * inv[12];
    if (det === 0) return this;
    det = 1 / det;
    _range(16).forEach((i: number) => {
      elements[i] = inv[i] * det;
    });
    return this;
  }

  public invert(): Matrix4 {
    return this.setInverseOf({ elements: this.elements });
  }

  public setOrthogonal({
                         top,
                         right,
                         bottom,
                         left,
                         near,
                         far
                       }: {
    top: number,
    right: number,
    bottom: number,
    left: number,
    near: number,
    far: number
  }): Matrix4 {
    if (left === right || bottom === top || near === far) {
      throw new Error('null frustum');
    }
    const rw = 1 / (right - left);
    const rh = 1 / (top - bottom);
    const rd = 1 / (far - near);

    const { elements } = this;
    [
      2 * rw, 0, 0, 0,
      0, 2 * rh, 0, 0,
      0, 0, -2 * rd, 0,
      -(right + left) * rw, -(top + bottom) * rh, -(far + near) * rd, 1,
    ].forEach((v, i) => elements[i] = v);

    return this;
  }

  public orthogonal(opt: { left, right, bottom, top, near, far }): Matrix4 {
    return this.multiply(new Matrix4().setOrthogonal(opt)) as Matrix4;
  }

  public setFrustum({ left, right, bottom, top, near, far }) {
    if (left === right || top === bottom || near === far) {
      throw new Error('null frustum');
    }

    if (near <= 0) {
      throw new Error('near <= 0');
    }

    if (far <= 0) {
      throw new Error('far <= 0');
    }

    const rw = 1 / (right - left);
    const rh = 1 / (top - bottom);
    const rd = 1 / (far - near);

    const { elements } = this;
    [
      2 * near * rw, 0, 0, 0,
      0, 2 * near * rh, 0, 0,
      (right + left) * rw, (top + bottom) * rh, -(far + near) * rd, 0,
      0, 0, -2 * near * far * rd, elements[15] = 0,
    ].forEach((v, i) => elements[i] = v);

    return this;
  }

  public frustum(opt: { left, right, bottom, top, near, far }): Matrix4 {
    return this.multiply(new Matrix4().setFrustum(opt)) as Matrix4;
  }

  public setPerspective({ fov, aspect, near, far, }:
                          { fov: number, aspect: number, near: number, far: number }): Matrix4 {
    if (near === far || aspect === 0) {
      throw new Error('null frustum');
    }

    if (near <= 0) {
      throw new Error('near <= 0');
    }

    if (far <= 0) {
      throw new Error('far <= 0');
    }

    const x: number = Math.PI * fov / 180 / 2;
    const s = Math.sin(x);
    if (s === 0) {
      throw new Error('null frustum');
    }
    const rd = 1 / (far - near);
    const ct = Math.cos(x) / s;
    const { elements } = this;

    [
      (ct / aspect), 0, 0, 0,
      0, ct, 0, 0,
      0, 0, -(far + near) * rd, -1,
      0, 0, -2 * near * far * rd, 0,
    ].forEach((v, i) => elements[i] = v);

    return this;
  }

  public perspective(opt: { fov, aspect, near, far }): Matrix4 {
    return this.multiply(new Matrix4().setPerspective(opt)) as Matrix4;
  }

  public setScale({ x, y, z, }): Matrix4 {
    const { elements } = this;
    [
      x, 0, 0, 0,
      0, y, 0, 0,
      0, 0, z, 0,
      0, 0, 0, 1,
    ].forEach((v, i) => elements[i] = v);

    return this;
  }

  public scale({ x, y, z }) {
    const { elements } = this;
    [
      x, x, x, x,
      y, y, y, y,
      z, z, z, z,
    ].forEach((v, i) => elements[i] *= v);

    return this;
  }

  public setTranslate({ x, y, z }): Matrix4 {
    const { elements } = this;
    [
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      x, y, z, 1
    ].forEach((v, i) => elements[i] = v);

    return this;
  }

  public translate({ x, y, z }: { x: number, y: number, z: number }): Matrix4 {
    const { elements: e } = this;
    e[12] += e[0] * x + e[4] * y + e[8] * z;
    e[13] += e[1] * x + e[5] * y + e[9] * z;
    e[14] += e[2] * x + e[6] * y + e[10] * z;
    e[15] += e[3] * x + e[7] * y + e[11] * z;
    return this;
  }

  public setRotate({ angle, x, y, z }: { angle: number, x: number, y: number, z: number }): Matrix4 {
    const { elements } = this;

    angle = Math.PI * angle / 180;
    let s = Math.sin(angle);
    const c = Math.cos(angle);

    if (0 !== x && 0 === y && 0 === z) {
      if (x < 0) {
        s = -s;
      }
      [
        1, 0, 0, 0,
        0, c, s, 0,
        0, -s, c, 0,
        0, 0, 0, 1
      ].forEach((v, i) => elements[i] = v);
    } else if (0 === x && 0 !== y && 0 === z) {
      if (y < 0) {
        s = -s;
      }
      [
        c, 0, -s, 0,
        0, 1, 0, 0,
        s, 0, c, 0,
        0, 0, 0, 1
      ].forEach((v, i) => elements[i] = v);
    } else if (0 === x && 0 === y && 0 !== z) {
      if (z < 0) {
        s = -s;
      }
      [
        c, s, 0, 0,
        -s, c, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1
      ].forEach((v, i) => elements[i] = v);
    } else {
      let len = Math.sqrt(x * x + y * y + z * z);
      if (len !== 1) {
        let rLen = 1 / len;
        x *= rLen;
        y *= rLen;
        z *= rLen;
      }
      const nc = 1 - c,
        xy = x * y, yz = y * z, zx = z * x,
        xs = x * s, ys = y * s, zs = z * s;
      [
        x * x * nc + c, xy * nc + zs, zx * nc - ys, 0,
        xy * nc - zs, y * y * nc + c, yz * nc + xs, 0,
        zx * nc + ys, yz * nc - xs, z * z * nc + c, 0,
        0, 0, 0, 1
      ].forEach((v, i) => elements[i] = v);
    }

    return this;
  }

  public rotate(opt: { angle, x, y, z }) {
    return this.multiply(new Matrix4().setRotate(opt));
  }

  public setLookAt({ eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ }:
                     {
                       eyeX: number, eyeY: number, eyeZ: number,
                       centerX: number, centerY: number, centerZ: number,
                       upX: number, upY: number, upZ: number
                     }) {
    let fx = centerX - eyeX;
    let fy = centerY - eyeY;
    let fz = centerZ - eyeZ;

    const rlf = 1 / Math.sqrt(fx * fx + fy * fy + fz * fz);
    fx *= rlf;
    fy *= rlf;
    fz *= rlf;

    let sx = fy * upZ - fz * upY;
    let sy = fz * upX - fx * upZ;
    let sz = fx * upY - fy * upX;

    let rls = 1 / Math.sqrt(sx * sx + sy * sy + sz * sz);
    sx *= rls;
    sy *= rls;
    sz *= rls;

    const ux = sy * fz - sz * fy;
    const uy = sz * fx - sx * fz;
    const uz = sx * fy - sy * fx;

    const { elements } = this;
    [
      sx, ux, -fx, 0,
      sy, uy, -fy, 0,
      sz, uz, -fz, 0,
      0, 0, 0, 1
    ].forEach((v, i) => elements[i] = v);

    return this.translate({ x: -eyeX, y: -eyeY, z: -eyeZ });
  }

  public lookAt(opt: {
    eyeX: number,
    eyeY: number,
    eyeZ: number,
    centerX: number,
    centerY: number,
    centerZ: number,
    upX: number,
    upY: number,
    upZ: number
  }): Matrix4 {
    return this.multiply(new Matrix4().setLookAt(opt));
  }

  public dropShadow({ plane, light }): Matrix4 {
    const mat = new Matrix4();
    const { elements } = mat;
    const dot = _range(4).reduce((prev, i) => plane[i] * light[i] + prev, 0);

    elements[0] = dot - light[0] * plane[0];
    elements[1] = -light[1] * plane[0];
    elements[2] = -light[2] * plane[0];
    elements[3] = -light[3] * plane[0];

    elements[4] = -light[0] * plane[1];
    elements[5] = dot - light[1] * plane[1];
    elements[6] = -light[2] * plane[1];
    elements[7] = -light[3] * plane[1];

    elements[8] = -light[0] * plane[2];
    elements[9] = -light[1] * plane[2];
    elements[10] = dot - light[2] * plane[2];
    elements[11] = -light[3] * plane[2];

    elements[12] = -light[0] * plane[3];
    elements[13] = -light[1] * plane[3];
    elements[14] = -light[2] * plane[3];
    elements[15] = dot - light[3] * plane[3];

    return this.multiply(mat) as Matrix4;
  }

  public dropShadowDirectionally({ normX, normY, normZ, planeX, planeY, planeZ, lightX, lightY, lightZ }) {
    const a = planeX * normX + planeY * normY + planeZ * normZ;
    const plane = [normX, normY, normZ, -a], light = [lightX, lightY, lightZ, 0];
    return this.dropShadow({ plane, light });
  }

  public multiply<T extends Vector3 | Vector4 | Matrix4>(vec: T): T {
    if (vec instanceof Vector3) {
      const { elements } = this;
      const { elements: v } = new Vector3();
      const { elements: result } = vec;

      result[0] = v[0] * elements[0] + v[1] * elements[4] + v[2] * elements[8] + elements[11];
      result[1] = v[0] * elements[1] + v[1] * elements[5] + v[2] * elements[9] + elements[12];
      result[2] = v[0] * elements[2] + v[1] * elements[6] + v[2] * elements[10] + elements[13];

      return vec;
    } else if (vec instanceof Vector4) {
      const { elements: e } = this;
      const { elements: result } = vec;
      const { elements: v } = new Vector4();

      result[0] = v[0] * e[0] + v[1] * e[4] + v[2] * e[8] + v[3] * e[12];
      result[1] = v[0] * e[1] + v[1] * e[5] + v[2] * e[9] + v[3] * e[13];
      result[2] = v[0] * e[2] + v[1] * e[6] + v[2] * e[10] + v[3] * e[14];
      result[3] = v[0] * e[3] + v[1] * e[7] + v[2] * e[11] + v[3] * e[15];

      return vec;
    } else {
      const { elements: e } = this;
      const a: Float32Array = e.slice();
      const { elements: b } = vec;

      if (e === b) {
        e.forEach((item, idx) => b[idx] = item);
      }

      [...Array(4).keys()].forEach(i => {
        let ai0 = a[i], ai1 = a[i + 4], ai2 = a[i + 8], ai3 = a[i + 12];
        e[i] = ai0 * b[0] + ai1 * b[1] + ai2 * b[2] + ai3 * b[3];
        e[i + 4] = ai0 * b[4] + ai1 * b[5] + ai2 * b[6] + ai3 * b[7];
        e[i + 8] = ai0 * b[8] + ai1 * b[9] + ai2 * b[10] + ai3 * b[11];
        e[i + 12] = ai0 * b[12] + ai1 * b[13] + ai2 * b[14] + ai3 * b[15];
      });
      return this as any;
    }
  }
}
